#!/usr/bin/env python
# encoding: utf-8
#
# Copyright (C) 2008 Nicolas Girard <nicolas@girard.name>
# Please view LICENSE for additional licensing information.
"""
A program converting restructured text into pdf presentations using the Powerdot LaTeX class.

Synopsis::

    tevisa [options] infile.txt
    
produces infile.pdf in the current directory

See <http:www.agapow.net/programming/python/rst2beamer> for more details.
"""

# TODO: More, err... *some* documentation is needed ;-)
# TODO: How to deal with \pdsetup ? 
# - Imitate the semantics with e.g. .. pdsetup:: ?
# - or decide that it should be put into a stylesheet file & included via --stylesheet ?

# TODO: What about verbatim text ?
# THANKS: to Martin Blais <blais@furius.ca> for rst2pdf, from which my latex2pdf() function was inpired
__docformat__ = 'restructuredtext en'
__author__ = "Nicolas Girard <nicolas@girard.name>"
__version__ = "0.5"

import locale,string, re
from docutils import frontend, io, nodes, languages
from docutils.core import default_description,default_usage,Publisher,publish_string
from docutils.parsers.rst import Directive,directives
from docutils.readers import standalone
from docutils.utils import SystemMessagePropagation
from docutils.writers.latex2e import Writer as Latex2eWriter
from docutils.writers.latex2e import LaTeXTranslator,Babel,Table

##### Directives

class Slide(Directive):
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = True
    has_content = True
    options = ('toc','bm','palette','method','dprop','dminsize','dmaxsize','dmindots','dmaxdots')
    option_spec = dict.fromkeys(options,directives.unchanged)
    klass='slide'

    def run(self):
        self.assert_has_content()
        text = '\n'.join(self.content)
        slide = nodes.container(text)
        slide['classes'].extend([self.klass])
        if self.arguments:
            slide['title']=self.arguments[0]
        else:
            slide['title']=None
        for k in self.options:
            if self.options.has_key(k):
                slide[k]=self.options[k]
        self.state.nested_parse(self.content, self.content_offset, slide)
        return [slide]

class WideSlide(Slide):
    klass='wideslide'

class EmptySlide(Directive):
    required_arguments = 0
    optional_arguments = 0
    final_argument_whitespace = True
    has_content = True

    def run(self):
        self.assert_has_content()
        text = '\n'.join(self.content)
        slide = nodes.container(text)
        slide['classes'].extend(['emptyslide'])
        self.state.nested_parse(self.content, self.content_offset, slide)
        return [slide]

class Contents(Directive):
    """
    Table of contents.

    The table of contents is generated in two passes: initial parse and
    transform.  During the initial parse, a 'pending' element is generated
    which acts as a placeholder, storing the TOC title and any options
    internally.  At a later stage in the processing, the 'pending' element is
    replaced by a 'topic' element, a title and the table of contents proper.
    """
    o_type_values=(0,1)
    def o_type(arg):
        return directives.choice(arg, Contents.o_type_values)

    o_content_values=('all', 'sections', 'currentsection','future','futuresections')
    def o_content(arg):
        return directives.choice(arg, Contents.o_content_values)
    
    required_arguments = 0
    optional_arguments = 1
    final_argument_whitespace = True
    option_spec = {'type': o_type,
                   'content': o_content}
    
    def run(self):
        if not (self.state_machine.match_titles
                or isinstance(self.state_machine.node, nodes.sidebar)):
            raise self.error('The "%s" directive may not be used within '
                             'topics or body elements.' % self.name)
        document = self.state_machine.document
        language = languages.get_language(document.settings.language_code)
        if self.arguments:
            title_text = self.arguments[0]
            text_nodes, messages = self.state.inline_text(title_text,
                                                          self.lineno)
            title = title_text
        else:
            messages = []
            if self.options.has_key('local'):
                title = None
            else:
                title = language.labels['contents']

        slide = nodes.container(None,classes=['slide'])
        slide['title'] = title
        slide['toc']=""

        topic = nodes.topic(classes=['contents'])
        #topic['classes'] += self.options.get('class', [])
        topic['type'] = self.options.get('type',0)
        topic['content'] = self.options.get('content','all')
        slide+=topic
        return [slide]


class TwoColumn(Directive):
    required_arguments = 0
    optional_arguments = 0
    has_content = True
    options=('lineheight','lineprop','lfrheight','lfrprop','rfrheight','rfrprop','lcolwidth','rcolwidth',
             'frsep','colsep','topsep','bottomsep','indent')
    option_spec = dict.fromkeys(options,directives.unchanged)

    def run(self):
        if not self.content:
            error = self.state_machine.reporter.error(
                'The "%s" directive is empty; content required.' % self.name,
                nodes.literal_block(self.block_text, self.block_text),
                line=self.lineno)
            return [error]
        node = nodes.Element()          # anonymous container for parsing
        self.state.nested_parse(self.content, self.content_offset, node)
        try:
            num_cols = self.check_list_content(node)
        except SystemMessagePropagation, detail:
            return [detail.args[0]]
        cols = [col for col in node[0]] # table_data
        twocolumn = nodes.container(None,classes=['twocolumn'])
        if self.options.has_key('lineheight'):
            twocolumn['lineheight'] = self.options['lineheight']
        for prop in ('lineprop','lfrprop','rfrprop'):
            if self.options.has_key(prop):
                val=self.options[prop]
                if val[0]=='{':
                    raise self.error(
                        'Error in "%s" directive: Don\'t put \'{\' \'}\' around the contents of the \'%s\' option.'%(prop,self.name))
                twocolumn[prop] = "{%s}"%val
        twocolumn['lcolwidth'] = self.options.get('lcolwidth','0.47\\linewidth')
        twocolumn['rcolwidth'] = self.options.get('rcolwidth','0.47\\linewidth')
        twocolumn['frsep'] = self.options.get('frsep','1.5mm')
        twocolumn['colsep'] = self.options.get('colsep','0.06\\linewidth')
        twocolumn['topsep'] = self.options.get('topsep','0cm')
        twocolumn['bottomsep'] = self.options.get('bottomsep','0cm')
        twocolumn['indent'] = self.options.get('indent','0cm')
        for col in cols:
            twocolumn+=nodes.compound(None,*col,**{'classes':['twocolumn']})
            #twocolumn+=col
        #table_node['classes'] += self.options.get('class', [])
        #if title:
        #    table_node.insert(0, title)
        return [twocolumn]

    def check_list_content(self, node):
        if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
            error = self.state_machine.reporter.error(
                'Error parsing content block for the "%s" directive: '
                'exactly one bullet list expected.' % self.name,
                nodes.literal_block(self.block_text, self.block_text),
                line=self.lineno)
            raise SystemMessagePropagation(error)
        list_node = node[0]
        if len(list_node)!=2:
            error = self.state_machine.reporter.error(
                'Error parsing content block for the "%s" directive: '
                'list containing exactly 2 items expected.' % self.name,
                nodes.literal_block(self.block_text, self.block_text),
                line=self.lineno)
            raise SystemMessagePropagation(error)
        return 2


directives.register_directive("slide", Slide)
directives.register_directive("wideslide", WideSlide)
directives.register_directive("emptyslide", EmptySlide)
directives.register_directive("contents", Contents)
directives.register_directive("twocolumn", TwoColumn)

#####

POWERDOT_SPEC =    (
    'Powerdot options',
    'Their syntax is the same than their equivalent from the Powerdot class',
    tuple (
        [
            (
                'Controls the kind of output that we want to produce. You can choose '
                'between "present", "print" and "handout". Default is "present".',
                ['--mode'],
                {'choices': ['present', 'print', 'handout'],
                'default': 'present', 'metavar': '<mode>'}
            ),
            (
                'Controls the size and ration of the output. You can choose '
                'between "screen", "a4paper" and "letterpaper". Default is "screen".',
                ['--paper'],
                {'choices': ['screen', 'a4paper', 'letterpaper'],
                'default': 'screen', 'metavar': '<paper>'}
            ),
            (
                'Controls the style to be loaded for the presentation. By default, the "default"'
                'style will be loaded. Available styles are: simple, tycja, ikeda, fyma, ciment,'
                'elcolors, aggie, husky, sailor, upen, bframe, horatio, paintings, klope, jefka, pazik',
                ['--style'],
                {'default': 'default', 'metavar': '<style>'}
            ),
            (
                'Controls the orientation of the presentation. You can choose '
                'between "portrait" and "landscape". Default is "landscape".',
                ['--orient'],
                {'choices': ['portrait', 'landscape'],
                'default': 'landscape', 'metavar': '<orient>'}
            ),
            (
                'Controls the production of slides and notes. You can choose '
                'between "slides", "slidesnotes" and "notes". Default is "slides".',
                ['--display'],
                {'choices': ['slides', 'slidesnotes', 'notes'],
                'default': 'slides', 'metavar': '<display>'}
            ),
            (
                'Size of the normal text font in points. Possible values are'
                '8pt, 9pt, 10pt, 11pt, 12pt, 14pt, 17pt and 20pt and the preset value is 11pt.',
                ['--size'],
                {'choices': ['8pt', '9pt', '10pt', '11pt', '12pt', '14pt', '17pt','20pt'],
                'default': '11pt', 'metavar': '<size>'}
            ),
            (
                'Makes equations flush left', 
                ['--fleqn'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
            (
                'Put equation numbers at the left', 
                ['--leqno'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
            (
                'If you experience problems with post processing or printing or you want to '
                'specify the paper size in the post processing steps yourself, use this option.', 
                ['--nopsheader'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
            (
                'Highlights table of contents entries when the entry matches with the'
                'current slide. Default is true.',
                ['--hlentries'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
            (
                'Highlights table of contents sections when the section matches with'
                'the current section in the presentation. Default is false.',
                ['--hlsections'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
           (
                'Add a black slide to the presentation', 
                ['--blackslide'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
           (
                'When in handout mode, let LaTeX decide on the places to insert a page break', 
                ['--nopagebreaks'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
           (
                'Displays a small digital clock on slides. Warning: due to a bug in hyperref, it is currently disabled', 
                ['--clock'],
                {'default': 0, 'action': 'store_true', 'validator': frontend.validate_boolean}
            ),
        ] + list (Latex2eWriter.settings_spec[2][2:])
    ),
)

POWERDOT_DEFAULTS = {
    #'output_encoding': 'latin-1',
    'use_latex_toc': 1,
    'use_latex_docinfo': 1,
    'documentclass': 'powerdot',
    'sectnum_xform': 0, # no section numbering, ignored by Powerdot anyway
}

REST_RAW_LATEX="""

.. role:: raw-latex(raw)
   :format: latex

"""

REST_PAUSE="""

.. |pause| replace:: :raw-latex:`\\pause`

"""

######

try:
     locale.setlocale (locale.LC_ALL, '')
except:
     pass


class PowerdotTranslator (LaTeXTranslator):
    """
    A converter for docutils elements to Powerdot-flavoured latex.
    """
    def __init__ (self, document):
        settings=document.settings
        d_options="mode=%s,paper=%s,orient=%s,display=%s,size=%s,style=%s,hlentries=%s,hlsections=%s"%(settings.mode,settings.paper,
            settings.orient,settings.display,settings.size,settings.style,bool(settings.hlentries),bool(settings.hlsections))
        for option in ('fleqn', 'leqno', 'nopsheader', 'blackslide'):
            if getattr(settings, option): 
                #print getattr(settings, option)
                d_options+=",%s"%option
        if settings.mode=='handout' and settings.nopagebreaks: d_options+=",nopagebreaks"
        if settings.clock: 
            pass
            # see: http://www.freelists.org/archives/powerdot/03-2007/msg00002.html
            #d_options+=",clock"
        settings.documentoptions=d_options
        LaTeXTranslator.__init__ (self, document)
        # These packages clash with Powerdot
        self.head_prefix = [x for x in self.head_prefix if not ('{typearea}' in x or '{hyperref}' in x)]
        # Restore the default fonts
        self.head_prefix = [x for x in self.head_prefix if not ('\usepackage{ae}' in x or '\usepackage{aeguill}' in x)]
        self.linking=None
        # To mimic Powerdot's behaviour
        self.date='\\today'
        self.item_extra=re.compile("^(<[\d-]+>)(\s+)(.*)")

    def bookmark(self, node):
        pass

    def visit_docinfo(self, node):
        pass

    def depart_docinfo(self, node):
        pass

    def visit_topic(self, node):
        if not ('contents' in node['classes'] and self.use_latex_toc):
            return LaTeXTranslator.visit_topic(self, node)
        self.body.append("\\tableofcontents[type=%d,content=%s]\n"%(node['type'],node['content']))
        raise nodes.SkipNode

    def visit_title(self, node):
        """Section and other titles."""
        if isinstance(node.parent, nodes.topic) \
        or isinstance(node.parent, nodes.sidebar) \
        or isinstance(node.parent, nodes.admonition) \
        or isinstance(node.parent, nodes.table) \
        or self.section_level == 0:
            return LaTeXTranslator.visit_title(self, node)
        self.body.append('\n\n')
        self.body.append('%' + '_' * 75)
        self.body.append('\n\n')
        self.bookmark(node)
        # Currently no \section* ; see last thead on the subject:
        # http://www.freelists.org/archives/powerdot/09-2005/msg00040.html
        #if self.document.settings.sectnum_xform:
        #    section_star = ""
        #else:
        #    section_star = "*"
        section_star = ""
        section_name = self.d_class.section(self.section_level)
        options=[]
        for c in node.parent['classes']:
            if c=='no-tocsection': options.append("tocsection=false")
            elif c=='hide-tocsection': options.append("tocsection=hidden")
            elif c=='no-slide': options.append("slide=false")
            elif c=='wideslide': options.append("template=wideslide")
        self.body.append('\\%s%s%s{' % (section_name,section_star,
            "[%s]"%','.join(options) if options else ''))
        # MAYBE postfix paragraph and subparagraph with \leavemode to
        # ensure floatables stay in the section and text starts on a new line.
        self.context.append('}\n')


    def depart_title(self, node):
        self.body.append(self.context.pop())
        #for id in node.parent['ids']:
        #    self.body.append('\\label{%s}\n' % id)

    def visit_container(self, node):
        if 'slide' in node['classes']:
            options=[]
            for k in Slide.options:
                if node.has_key(k):
                    options.append("%s=%s"%(k,node[k] or ""))
            self.body.append('\n\\begin{slide}%s{%s}\n'%(
                "[%s]"%','.join(options) if options else '',
                node['title']))
        elif 'wideslide' in node['classes']: # TODO: DRY !!!
            options=[]
            for k in Slide.options:
                if node.has_key(k):
                    options.append("%s=%s"%(k,node[k] or ""))
            self.body.append('\n\\begin{wideslide}%s{%s}\n'%(
                "[%s]"%','.join(options) if options else '',
                node['title']))
        elif 'emptyslide' in node['classes']:
            self.body.append('\n\\begin{emptyslide}{}\n')
        elif 'twocolumn' in node['classes']:
            options=[]
            for k in TwoColumn.options:
                if node.has_key(k):
                    options.append("%s=%s"%(k,node[k] or ""))
            self.body.append('\\twocolumn%s'%"[%s]"%','.join(options) if options else '')
        else:
            return LaTeXTranslator.visit_container(self, node)

    def depart_container(self, node):
        if 'slide' in node['classes']:
            self.body.append('\n\\end{slide}\n\n')
        elif 'wideslide' in node['classes']:
            self.body.append('\n\\end{wideslide}\n\n')
        elif 'emptyslide' in node['classes']:
            self.body.append('\n\\end{emptyslide}\n\n')
        elif 'twocolumn' in node['classes']:
            self.body.append('\n\n')
        else:
            return LaTeXTranslator.depart_container(self, node)

    def visit_compound(self, node):
        if 'twocolumn' in node['classes']:
            self.body.append('{')
        else:
            return LaTeXTranslator.visit_compound(self, node)

    def depart_compound(self, node):
        if 'twocolumn' in node['classes']:
            self.body.append('}')
        else:
            return LaTeXTranslator.depart_compound(self, node)

    def visit_note(self, node):
        self.body.append('\\begin{note}{%s}'%'') # TODO:  How to implement note title ? cf powerdot.pdf, p.16

    def depart_note(self, node):
        self.body.append('\\end{note}\n\n')

    def visit_list_item(self, node):
        # Append "{}" in case the next character is "[", which would break
        # LaTeX's list environment (no numbering and the "[" is not printed).
        child = node[0]
        if child.__class__==nodes.paragraph:
            if len(child):
                subchild=child[0]
                data=subchild.data
                m = self.item_extra.match(data)
                if m:
                    self.body.append('\\item%s {}%%'%m.group(1))
                    subchild.data=m.group(3)
                    return
        self.body.append('\\item {}%%')

    def visit_enumerated_list(self, node):
        # We create our own enumeration list environment. -> clashes with Powerdot
        self.body.append('\\begin{enumerate}%s\n'%('[type=1]' if 'show-inactive' in node['classes'] else ''))

    def visit_bullet_list(self, node):
        if 'contents' in self.topic_classes:
            return LaTeXTranslator.visit_bullet_list(self, node)
        else:
            self.body.append( '\\begin{itemize}%s\n'%('[type=1]' if 'show-inactive' in node['classes'] else '') )

    def depart_enumerated_list(self, node):
        self.body.append('\\end{enumerate}\n')

    def depart_document(self, node):
        LaTeXTranslator.depart_document(self, node)
        # Undocumented, dunno if useful
        self.body_suffix.append('\\endinput\n')


class PowerdotWriter (Latex2eWriter):
    """
    A docutils writer that modifies the translator and settings for Powerdot.
    """
    settings_spec = POWERDOT_SPEC
    settings_defaults = POWERDOT_DEFAULTS
    
    def __init__(self):
        Latex2eWriter.__init__(self)
        self.translator_class = PowerdotTranslator


def gprint(s):
    # TODO: use docutils' log functions
    pass


import os, tempfile, shutil
from os.path import basename,dirname,isdir,join,splitext
from subprocess import Popen, call, PIPE

class InvokeException(Exception):
    def __init__(self,command):
        self.command = command
    def __str__(self):
        return "%s: Non-zero exit status."%self.command

def invoke(command,cwd,*args):
    # .stdout and .stderr are None when not redirected
    p = Popen(([command]+list(args)), cwd=cwd,stdout=PIPE, stderr=PIPE)
    rc = p.wait()
    if rc != 0:
        raise InvokeException(command)

def latex2pdf(output, src, dest=None):
    # TODO: images: relative path
    gprint("latex2pdf(self,%s, %s, %s)"%("output", src, dest))
    # stdlib imports

    # ugly  :-/
    if not src: src='./out.txt'
    # If not specified, compute the name of the destination file.
    srcbase = splitext(src)[0]
    if dest is None:
        dest = srcbase + '.pdf'
    elif isdir(dest):
        dest = join(dest, srcbase + '.pdf')

    # Write the LaTeX output file to a temporary file in the same directory as
    # the input file.  We have to use that directory because of the possibility
    # of relative filenames/included files.
    cwd = dirname(src) or os.getcwd()
    pre_files = os.listdir(cwd)
    f = tempfile.NamedTemporaryFile('w', 
        dir=cwd, prefix='rst2pdf-', suffix='.tex')
    f.write(output)
    f.flush()
    tex_fn = basename(f.name)
    tmp_base_fname=splitext(f.name)[0]
    log_fn = tmp_base_fname + '.log'
    tmp_prefix = splitext(tex_fn)[0]
    def anotherRunNeeded(log_fn):
        needed=False
        log_f = open(log_fn)
        latex_warning="LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right."
        for line in log_f:
            if line.startswith(latex_warning):
                needed=True
                break
        log_f.close()
        return needed
    try:
        run_needed=True
        while run_needed:
            invoke('latex',cwd,'--shell-escape', '-interaction=nonstopmode', f.name)
            run_needed = anotherRunNeeded(log_fn)
            if run_needed:
                gprint("Performing another run to get cross-references right")
        dvi_fn = tmp_base_fname+'.dvi'
        ps_fn  = tmp_base_fname+'.ps'
        pdf_fn = tmp_base_fname+'.pdf'
        invoke('dvips',cwd,'-o',ps_fn,dvi_fn)
        invoke('ps2pdf',cwd,ps_fn,pdf_fn)
        shutil.copyfile(pdf_fn, dest)
    except InvokeException,e:
        if e.command=='latex':
            log_f = open(log_fn)
            gprint("Error: Non-zero exit status.")
            interesting=False
            still_interesting=True
            for line in log_f:
                if not interesting and (line.startswith('LaTeX Warning:') or line.startswith('! ')): interesting=True 
                if interesting: 
                    if still_interesting and (line.startswith("Here is how much of TeX's memory you used:") or \
                                              line.startswith("Type X to quit or <RETURN> to proceed,")):
                        still_interesting=False
                    if still_interesting and line.strip():
                        print "%s: %s"%("LaTeX log",line)
            log_f.close()
        else:
            print e
        exit(-1)
    finally:
        # Clean up the temporary files created by the process.
        post_files = os.listdir(cwd)
        diff_files = frozenset(post_files) - frozenset(pre_files)
        for fn in diff_files:
            if fn == tex_fn:
                continue
            if fn.startswith(tmp_prefix):
                gprint("Deleting '%s'" % fn)
                os.unlink(join(cwd, fn))
    # Delete the temporary LaTeX file.
    f.close()
    del f

description = ("""
A program converting restructured text into pdf presentations using the Powerdot LaTeX class.
""")

def publish_cmdline(reader=None, reader_name='standalone',
                    parser=None, parser_name='restructuredtext',
                    writer=None, writer_name='pseudoxml',
                    settings=None, settings_spec=None,
                    settings_overrides=None, config_section=None,
                    enable_exit_status=1, argv=None,
                    usage=default_usage, description=default_description,
                    destination=None): 
    pub = Publisher(reader, parser, writer, settings=settings,#)#,
                    destination_class=io.StringOutput) # ngd: added last line
    pub.set_components(reader_name, parser_name, writer_name)
    pub.process_command_line(argv,usage,description,settings_spec,config_section,**(settings_overrides or {}))
    output = pub.publish(
        argv, usage, description, settings_spec, settings_overrides,
        config_section=config_section, enable_exit_status=enable_exit_status)
    return output, pub.settings._source, pub.settings._destination


class PowerdotReader(standalone.Reader):
    def read(self, source, parser, settings):
        self.source = source
        if not self.parser:
            self.parser = parser
        self.settings = settings
        self.input = REST_RAW_LATEX+self.source.read()+REST_PAUSE
        self.parse()
        return self.document

def main():
    output, src, dest = publish_cmdline (reader=PowerdotReader(),writer=PowerdotWriter(), 
        description=description,settings_overrides={'exit_status_level': 3})
    latex2pdf(output, src, dest)
    #print output

if __name__ == '__main__':
    main()
